﻿/// <reference path="jquery.d.ts" />
/// <reference path="backbone.d.ts" />
/// <reference path="datepicker.d.ts" />
/// <reference path="spiro-models.ts" />
/// <reference path="tags.ts" />

//  Spiro - Classic is a generic client for Restful Objects

declare var _: any;

module Spiro.Classic {  // change to Classic 

    var waitCount = 0;

    export function cursorWait() {
        waitCount++;
        document.body.style.cursor = 'progress';
    }

    export function cursorReset() {
        waitCount = 0;
        cursorClear();
    }

    // Returns the cursor to the default pointer
    export function cursorClear() {
        if (--waitCount <= 0) {
            document.body.style.cursor = 'auto';
        }
    }


    var home: Spiro.HomePageRepresentation;

    // helpers 

    function toISODate(d: Date): string {

        function pad(n: number) {
            return n < 10 ? '0' + n : n.toString();
        }

        if (isNaN(d.getTime())) {
            return "";
        }

        return d.getUTCFullYear() + '-'
            + pad(d.getUTCMonth() + 1) + '-'
                + pad(d.getUTCDate()) + 'T'
                    + pad(d.getUTCHours()) + ':'
                        + pad(d.getUTCMinutes()) + ':'
                            + pad(d.getUTCSeconds()) + 'Z';
    }

    function toKeyValueArray(map: Object) {
        var fieldsArray = [];

        for (var key in map) {
            if (map.hasOwnProperty(key)) {
                fieldsArray.push({
                    key: key,
                    value: map[key]
                });
            }
        }
        return fieldsArray;
    }

    function getFieldInput(property: JQuery): Value {
        var input = property.find('.nof-value > :input');
        var newValue: any = null;

        if (input.length > 0) {
            newValue = input.val();

            if (input.hasClass('date')) {
                var dt = new Date(newValue);
                newValue = toISODate(dt);
            }
            newValue = $.trim(newValue);

        } else {
            var link = property.find(".nof-object > a");

            if (link.length > 0 && link.attr("href")) {
                newValue = new Link({ "href": link.attr("href") });
            }
        }

        return new Value(newValue);
    }

    // clear main and return tag
    function displayOnMain(): JQuery {
        $("#main").html("");
        return $("#main");
    }

    function friendlyActionName(member: Spiro.Member);
    function friendlyActionName(member: Spiro.ActionRepresentation);

    // Adds an elipsis if the action has params
    function friendlyActionName(member: any) {
        var name: string = member.extensions().friendlyName;
        if (member.extensions().hasParams === true) {
            name += "...";
        }
        return name;
    }

    function mandatoryIndicatorIfApplies(member: Spiro.Member);
    function mandatoryIndicatorIfApplies(member: Spiro.Parameter);

    function mandatoryIndicatorIfApplies(member: any) {
        if (member.extensions().optional === false) {
            var spanTag = Tags.Tag("span", "nof-mandatory-field-indicator");
            spanTag.setInnerText("*");
            return spanTag.toString();
        }
        return "";
    }

    function formattedDateTime(value): string {

        var pad = function (toPad) {
            var s = toPad.toString();

            if (s.length == 1) {
                return "0" + toPad;
            }
            return toPad;
        };

        if (value === null) {
            return "";
        }

        var date = new Date(value);

        return $.datepicker.formatDate('dd/mm/yy', date) + " " + pad(date.getHours()) + ":" + pad(date.getMinutes()) + ":" + pad(date.getSeconds());
    }

    function formattedScalar(value): string {
        if (value === null) {
            return "";
        }

        return value.toString();
    }

    function shortName(value: string) {
        return value.substring(value.lastIndexOf(".") + 1, value.length);
    }

    function isFindAction(object: ActionRepresentation, returnType: string) {
        if (object.extensions().returnType === returnType || object.extensions().elementType === returnType) {

            if (object.extensions().hasParams) {

                return _.all(_.toArray(object.parameters()), function (i: Parameter) {

                    return i.extensions().returnType === "integer" ||
                        i.extensions().returnType === "string" ||
                            i.extensions().returnType === "number" ||
                                i.extensions().returnType === "boolean";
                });
            }

            return true;
        }
        return false;
    }

    class DomainObjectListItemView extends Backbone.View {
        tagName = "tr";
        $parentEl: JQuery;

        constructor (public model: Spiro.DomainObjectRepresentation, options? ) {
            super(options);

            this.el = undefined;
            this._ensureElement();

            _.bindAll(this, 'render', 'selfLink', 'refLink');
            this.model.bind('change', this.render);
            this.model.bind('reset', this.render);

            this.delegateEvents();
        }

        events = {
            "click .selfLink": "selfLink",
            "click .refLink": "refLink"
        }

        selfLink(e: JQueryEventObject) {
            var view = new DomainObjectView(this.model);
            view.$parentEl = displayOnMain();
            view.render();
            return false;
        }

        refLink(e: JQueryEventObject) {
            var currentTarget = <HTMLElement> e.currentTarget;

            var refLink =  this.model.propertyMembers()[currentTarget.id].value().link();
            var selectedModel = <DomainObjectRepresentation> refLink.getTarget();

            var view = new DomainObjectView(selectedModel);
            view.$parentEl = displayOnMain();
            selectedModel.fetch();
            return false;
        }

        renderRow(): string {

            var selfLinkTag = Tags.Tag("a", "selfLink", this.model.title());
            selfLinkTag.mergeAttribute("id", this.model.instanceId());
            selfLinkTag.mergeAttribute("href", this.model.url());

            var selfDivTag = Tags.Tag("div", "nof-object");
            selfDivTag.innerHtml += Tags.Tag("img").toString("SelfClosing");
            selfDivTag.innerHtml += selfLinkTag;

            var selfTag = Tags.Tag("td", "", selfDivTag);

            var fields = "";

            for (var p in this.model.members()) {
                var itemTag = Tags.Tag("td");
                var itemDivTag = Tags.Tag("div");
                var member = this.model.members()[p];

                if (member instanceof PropertyMember) {
                    var property = <PropertyMember>member;
                    if (property.isScalar()) {
                        itemDivTag.addCssClass("nof-value");
                        itemDivTag.setInnerText(property.extensions().returnType === "date-time" ? formattedDateTime(property.value()) : formattedScalar(property.value()));
                    }
                    else {
                        itemDivTag.addCssClass("nof-object");

                        if (property.value()) {
                            var imgTag = Tags.Tag("img");
                            imgTag.mergeAttribute("src", "/Content/Default.png").mergeAttribute("alt", "");
                            itemDivTag.innerHtml += imgTag;

                            var linkTag = Tags.Tag("a", "refLink", property.value().toString());
                            linkTag.mergeAttribute("id", p).mergeAttribute("href", property.value().link().href());

                            itemDivTag.innerHtml += linkTag;
                        }
                    }
                    itemTag.innerHtml += itemDivTag;
                    fields += itemTag;
                }
                if (member instanceof CollectionMember) {
                    var collection = <CollectionMember>member;
                    itemDivTag.addCssClass("nof-object");
                    var title = (collection.size() == 0 ? "No" : collection.size().toString()) + " " + collection.extensions().pluralName;
                    itemDivTag.setInnerText(title);
                    itemTag.innerHtml += itemDivTag;
                    fields += itemTag;
                }
            }

            return selfTag.toString() + fields;
        }

        renderHeader(): string {
            var fields = "";

            for (var p in this.model.members()) {
                var itemTag = Tags.Tag("th");

                var member = this.model.members()[p];

                if (member instanceof PropertyMember || member instanceof CollectionMember) {
                    itemTag.setInnerText(p);
                    fields += itemTag;
                }
            }
            return Tags.Tag("th").toString("SelfClosing") + fields;
        }

        render(): DomainObjectListItemView {
            this.$el.html(this.renderRow());

            this.$parentEl.find("tr:first").html(this.renderHeader());
            this.$parentEl.append(this.$el);

            return this;
        }
    }

    class ListTableView extends Backbone.View {
        tagName = "div";
        className = "nof-collection-table";
        $parentEl: JQuery;

        constructor (public model: Spiro.Links, options? ) {
            super(options);

            this.el = undefined;
            this._ensureElement();

            _.bindAll(this, 'render');
            this.model.bind('change', this.render);
            this.model.bind('reset', this.render);

            this.delegateEvents();
        }

        renderInlineTable(): string {
            return Tags.Tag("table", "", Tags.Tag("tbody", "", Tags.Tag("tr"))).toString();
        }

        render(): ListTableView {
            this.$parentEl.find(".nof-collection-table").remove();

            this.$el.html(this.renderInlineTable());
            this.$parentEl.append(this.$el);

            _.each(this.model.models, function (itemLink: Link) {
                var itemModel = <DomainObjectRepresentation> itemLink.getTarget();
                var itemView = new DomainObjectListItemView(itemModel);
                itemView.$parentEl = this.$el.find("tbody");
                itemModel.fetch();
            }, this);

            return this;
        }
    }

    class DomainObjectListView extends Backbone.View {
        tagName = "div";
        className = "nof-standalonetable";

        constructor (public model: Spiro.Links, options? ) {
            super(options);

            this.el = undefined;
            this._ensureElement();

            _.bindAll(this, 'render');
            this.model.bind('change', this.render);
            this.model.bind('reset', this.render);

            this.delegateEvents();
        }

        renderTable(): string {
            var count = this.model.models.length.toString();
            return Tags.Tag("div", "nof-object", "Viewing " + count + " of " + count).toString() + Tags.Tag("form", "nof-action", Tags.Tag("div")).toString();
        }

        render(): DomainObjectListView {
            this.$el.html(this.renderTable());

            displayOnMain().html(this.$el);

            var view = new ListTableView(this.model);
            view.$parentEl = this.$el.find("> form > div");
            view.render();

            return this;
        }
    }

    class DomainObjectLinkView extends Backbone.View {
        tagName = "a";
        $parentEl: JQuery;

        constructor (public model: Spiro.DomainObjectRepresentation, options? ) {
            super(options);

            this.el = undefined;
            this._ensureElement();

            _.bindAll(this, 'render');
            this.model.bind('change', this.render);
            this.model.bind('reset', this.render);

            this.delegateEvents();
        }

        renderImgTag(): string {
            var tag = Tags.Tag("img");
            tag.mergeAttribute("src", "/Content/Default.png").mergeAttribute("alt", "");
            return tag.toString();
        }


        render(): DomainObjectLinkView {
            this.$el.attr("href", this.model.url());
            this.$el.html(this.model.title());

            // clear any existing links or warnings 
            this.$parentEl.find(" > a").remove();
            this.$parentEl.find(" > img").remove();
            this.$parentEl.find(".nof-collection-list").remove();
            this.$parentEl.find(".field-validation-error").remove();

            this.$parentEl.prepend(this.$el); // before mandatory ind ie '*' 
            this.$parentEl.prepend(this.renderImgTag());


            return this;
        }
    }

    class ListLinkView extends Backbone.View {
        tagName = "div";
        className = "nof-collection-list";
        $parentEl: JQuery;

        constructor (public model: Spiro.Links, options? ) {
            super(options);

            this.el = undefined;
            this._ensureElement();

            _.bindAll(this, 'render');
            this.model.bind('change', this.render);
            this.model.bind('reset', this.render);

            this.delegateEvents();
        }

        renderInlineList(): string {
            var bodyTag = Tags.Tag("tbody");
            var tableTag = Tags.Tag("table", "", bodyTag);

            for (var i = 0; i < this.model.models.length; i++) {
                var item = this.model.models[i];

                var linkTag = Tags.Tag("a", "", "Details");
                linkTag.mergeAttribute("href", item.href());

                var divTag = Tags.Tag("div", "", item.title());
                divTag.innerHtml += linkTag;
                divTag.innerHtml += Tags.Tag("button", "", "Select");

                bodyTag.innerHtml += Tags.Tag("tr", "", Tags.Tag("td", "", divTag));
            }
            return tableTag.toString();
        }

        render(): ListLinkView {
            this.$parentEl.find(".nof-collection-list").remove();

            this.$el.append(this.renderInlineList());
            this.$parentEl.append(this.$el);

            return this;
        }
    }

    class PropertyView  extends Backbone.View {

        constructor (options? ) {
            super(options);
        }

        memberId;
        editMode: bool;

        static createView(model: Spiro.Member): PropertyView {
            var view: PropertyView;

            if (model.memberType() === "property" && model.isScalar()) {
                view = new PropertyValueView(<PropertyMember>  model);
            } else if (model.memberType() === "property") {
                view = new PropertyReferenceView(<PropertyMember>  model);
            } else if (model.memberType() === "collection") {
                view = new PropertyCollectionView(<CollectionMember>  model);
            }
            return view;
        }

        render(): PropertyView {
            return this;
        };
    }

    class PropertyValueView extends PropertyView {
        tagName = "div";
        className = "nof-property";


        constructor (public model: Spiro.PropertyMember, options? ) {
            super(options);

            this.el = undefined;
            this._ensureElement();

            _.bindAll(this, 'render');

        }

        renderEditScalarProperty(): string {
            if (this.model.disabledReason()) {
                return this.renderViewScalarProperty();
            }

            var inputTag = Tags.Tag("input", "");

            if (this.model.extensions().format === "date-time") {
                inputTag.addCssClass("date");
                inputTag.mergeAttribute("value", formattedDateTime(this.model.value()));
            }
            else {
                inputTag.mergeAttribute("value", formattedScalar(this.model.value()));
            }

            inputTag.innerHtml += mandatoryIndicatorIfApplies(this.model);

            return Tags.Tag("label", "", this.model.extensions().friendlyName).toString() +
                   Tags.Tag("div", "nof-value", inputTag).toString();

        }

        renderViewScalarProperty(): string {
            return Tags.Tag("label", "", this.model.extensions().friendlyName).toString() +
                   Tags.Tag("div", "nof-value", this.model.extensions().format === "date-time" ? formattedDateTime(this.model.value()) : formattedScalar(this.model.value()));
        }


        render(): PropertyValueView {

            if (this.editMode) {
                this.$el.append(this.renderEditScalarProperty());
            } else {
                this.$el.append(this.renderViewScalarProperty());
            }

            this.$el.attr("id", this.memberId);

            return this;
        }
    }

    class PropertyReferenceView extends PropertyView {
        tagName = "div";
        className = "nof-property";
        mode: string = "minimized";

        constructor (public model: Spiro.PropertyMember, options? ) {
            super(options);

            this.el = undefined;
            this._ensureElement();

            _.bindAll(this, 'render', "link");


            this.delegateEvents();
        }

        events = {
            "click :button": "select",
            "click .nof-object > a": "link"
        }

        link(e: JQueryEventObject) {

            // find matching model 
            var currentTarget = <HTMLElement> e.currentTarget;

            var refLink = this.model.value().link();
            var selectedModel = <DomainObjectRepresentation> refLink.getTarget();

            var view = new DomainObjectView(selectedModel);
            view.$parentEl = displayOnMain();
            selectedModel.fetch();
            return false;
        }


        maximize() {
            this.mode = "maximized";
            this.render();
        }

        minimize() {
            this.mode = "minimized";
            this.render();
        }

        select(e: JQueryEventObject) {

            var currentTarget = <HTMLElement> e.currentTarget;
            if (currentTarget.className === "nof-maximize") {
                this.maximize();
            } else if (currentTarget.className === "nof-minimize") {
                this.minimize();
            }
            return false;
        }

        renderEditReferenceProperty(): string {

            var objectTag = Tags.Tag("div", "nof-object");
            objectTag.mergeAttribute("data-type", this.model.extensions().returnType);

            if (!(this.model.disabledReason())) {
                // add finder for non disabled references 
                objectTag.mergeAttribute("data-hasFinder", "true");
            }


            if (this.model.value()) {
                var imgTag = Tags.Tag("img");
                imgTag.mergeAttribute("src", "/Content/Default.png").mergeAttribute("alt", "");

                var linkTag = Tags.Tag("a", "", this.model.value().toString());
                linkTag.mergeAttribute("id", this.memberId).mergeAttribute("href", this.model.value().link().href());

                objectTag.innerHtml += imgTag;
                objectTag.innerHtml += linkTag;
            }

            return Tags.Tag("label", "", this.model.extensions().friendlyName).toString() + objectTag;
        }

        renderViewReferenceProperty(): string {

            var objectTag = Tags.Tag("div", "nof-object");
            objectTag.mergeAttribute("data-type", this.model.extensions().returnType);

            if (!this.model.value().isNull()) {
                var imgTag = Tags.Tag("img");
                imgTag.mergeAttribute("src", "/Content/Default.png").mergeAttribute("alt", "");

                var linkTag = Tags.Tag("a", "", this.model.value().toString());
                linkTag.mergeAttribute("id", this.memberId).mergeAttribute("href", this.model.value().link().href());

                var buttonMinTag = Tags.Tag("button", "nof-minimize", "Collapse");
                var buttonMaxTag = Tags.Tag("button", "nof-maximize", "Expand");

                buttonMinTag.mergeAttribute("style", "display: none;");
                buttonMaxTag.mergeAttribute("style", "display: inline;");

                var buttonDivTag = Tags.Tag("div");
                buttonDivTag.innerHtml += buttonMinTag;
                buttonDivTag.innerHtml += buttonMaxTag;

                var formTag = Tags.Tag("form", "", buttonDivTag);
                formTag.mergeAttribute("id", this.memberId);

                objectTag.innerHtml += imgTag;
                objectTag.innerHtml += linkTag;
                objectTag.innerHtml += formTag;
            }

            return Tags.Tag("label", "", this.model.extensions().friendlyName).toString() + objectTag;
        }

        render(): PropertyReferenceView {

            if (this.editMode) {
                this.$el.html(this.renderEditReferenceProperty());
            } else {
                this.$el.html(this.renderViewReferenceProperty());

                if (this.mode === "minimized") {
                    this.$el.find(".nof-maximize").show();
                    this.$el.find(".nof-minimize").hide();

                    this.$el.find(".nof-propertylist").remove();
                }
                if (this.mode === "maximized") {
                    this.$el.find(".nof-maximize").hide();
                    this.$el.find(".nof-minimize").show();

                    var refLink = this.model.value().link();
                    var propertyValue =  <DomainObjectRepresentation> refLink.getTarget();

                    var view = new DomainObjectPropertiesView(propertyValue);
                    view.$parentEl = this.$el.find(".nof-object");
                    propertyValue.fetch();
                }

            }

            this.$el.attr("id", this.memberId);

            return this;
        }
    }

    class PropertyCollectionView  extends PropertyView {
        tagName = "div";
        className = "nof-property";
        mode: string = "summary";
        memberId;

        constructor (public model: Spiro.CollectionMember, options? ) {
            super(options);

            this.el = undefined;
            this._ensureElement();
            _.bindAll(this, 'render');


            this.delegateEvents();
        }

        events = {
            "click :button": "select"
        }

        summarize() {
            this.mode = "summary";
            this.render();
        }

        listify() {
            this.mode = "list";
            this.render();
        }

        tabulate() {
            this.mode = "table";
            this.render();
        }

        select(e: JQueryEventObject) {

            var currentTarget = <HTMLElement> e.currentTarget;
            if (currentTarget.className === "nof-summary") {
                this.summarize();
            } else if (currentTarget.className === "nof-list") {
                this.listify();
            } else if (currentTarget.className === "nof-table") {
                this.tabulate();
            }

            return false;
        }

        renderViewCollectionProperty(): string {

            var objectTag = Tags.Tag("div", "nof-collection-summary");

            var buttonSummaryTag = Tags.Tag("button", "nof-summary", "Summary");
            var buttonListTag = Tags.Tag("button", "nof-list", "List");
            var buttonTableTag = Tags.Tag("button", "nof-table", "Table");

            buttonSummaryTag.mergeAttribute("style", "display: none;");
            buttonListTag.mergeAttribute("style", "display: inline;");
            buttonTableTag.mergeAttribute("style", "display: inline;");

            var buttonDivTag = Tags.Tag("div");
            buttonDivTag.innerHtml += buttonSummaryTag;
            buttonDivTag.innerHtml += buttonListTag;
            buttonDivTag.innerHtml += buttonTableTag;

            var formTag = Tags.Tag("form", "", buttonDivTag);
            formTag.mergeAttribute("id", this.memberId);

            var summaryTag = Tags.Tag("div", "nof-object", (this.model.size() == 0 ? "No" : this.model.size().toString()) + " " + this.model.extensions().pluralName);

            objectTag.innerHtml += formTag;
            objectTag.innerHtml += summaryTag;

            return Tags.Tag("label", "", this.model.extensions().friendlyName).toString() + objectTag;
        }


        render(): PropertyCollectionView {

            var view;

            this.$el.html(this.renderViewCollectionProperty());

            var cDiv = this.$el.find(".nof-collection-summary, .nof-collection-list, .nof-collection-table");

            if (this.mode === "summary") {
                this.$el.find(".nof-summary").hide();
                this.$el.find(".nof-list").show();
                this.$el.find(".nof-table").show();

                cDiv.addClass("nof-collection-summary");
                cDiv.removeClass("nof-collection-list");
                cDiv.removeClass("nof-collection-table");
            }

            if (this.mode === "list") {
                this.$el.find(".nof-summary").show();
                this.$el.find(".nof-list").hide();
                this.$el.find(".nof-table").show();

                cDiv.addClass("nof-collection-list");
                cDiv.removeClass("nof-collection-summary");
                cDiv.removeClass("nof-collection-table");

                this.$el.find(".nof-object").remove();

                var propertyValue = this.model.getDetails();

                view = new InlineListCollectionView(propertyValue);
                view.$parentEl = cDiv;
                propertyValue.fetch();
            }

            if (this.mode === "table") {
                this.$el.find(".nof-summary").show();
                this.$el.find(".nof-list").show();
                this.$el.find(".nof-table").hide();

                cDiv.addClass("nof-collection-table");
                cDiv.removeClass("nof-collection-list");
                cDiv.removeClass("nof-collection-summary");

                this.$el.find(".nof-object").remove();

                var propertyValue = this.model.getDetails();

                view = new InlineTableCollectionView(propertyValue);
                view.$parentEl = cDiv;
                propertyValue.fetch();
            }

            this.$el.attr("id", this.memberId);

            return this;
        }
    }

    class InlineListCollectionView extends Backbone.View {
        tagName = "table";
        $parentEl: JQuery;

        constructor (public model: Spiro.CollectionRepresentation, options? ) {
            super(options);

            this.el = undefined;
            this._ensureElement();
            _.bindAll(this, 'render');
            this.model.bind('change', this.render);
            this.model.bind('reset', this.render);

            this.delegateEvents();
        }

        events = {
            "click .nof-object > a": "link"
        }

        link(e: JQueryEventObject) {

            var currentTarget = <HTMLAnchorElement> e.currentTarget;

            var selectedLink = _.find(this.model.value().models, function (i: Link) {
                return i.href() === currentTarget.href;
            });

            var selectedModel = selectedLink.getTarget();
            var view = new DomainObjectView(selectedModel);
            view.$parentEl = displayOnMain();
            selectedModel.fetch();
            return false;
        }

        renderInlineCollectionList(): string {

            var bodyTag = Tags.Tag("tbody");

            for (var i = 0; i < this.model.value().models.length; i++) {
                var item = this.model.value().models[i];

                var imgTag = Tags.Tag("img");
                imgTag.mergeAttribute("src", "/Content/Default.png").mergeAttribute("alt", "");

                var linkTag = Tags.Tag("a", "", item.title());
                linkTag.mergeAttribute("href", item.href());

                var divTag = Tags.Tag("div", "nof-object");
                divTag.innerHtml += imgTag;
                divTag.innerHtml += linkTag;

                bodyTag.innerHtml += Tags.Tag("tr", "", Tags.Tag("td", "", divTag));
            }
            return bodyTag.toString();
        }


        render(): InlineListCollectionView {
            this.$el.html(this.renderInlineCollectionList());
            this.$parentEl.append(this.$el);
            return this;
        }
    }

    class InlineTableCollectionView extends Backbone.View {
        tagName = "table";
        $parentEl: JQuery;

        constructor (public model: Spiro.CollectionRepresentation, options? ) {
            super(options);

            this.el = undefined;
            this._ensureElement();
            _.bindAll(this, 'render');
            this.model.bind('change', this.render);
            this.model.bind('reset', this.render);

            this.delegateEvents();
        }

        renderInlineCollectionTable(): string {
            return Tags.Tag("tBody", "", Tags.Tag("tr")).toString();
        }

        render(): InlineTableCollectionView {
            this.$el.html(this.renderInlineCollectionTable());

            this.$parentEl.append(this.$el);

            _.each(this.model.value().models, function (itemLink: Link) {
                var itemModel =  <DomainObjectRepresentation> itemLink.getTarget();
                var item = new DomainObjectListItemView(itemModel);
                item.$parentEl = this.$el.find("tbody");
                itemModel.fetch();
            }, this);


            return this;
        }
    }

    class DomainObjectPropertiesView extends Backbone.View {
        tagName = "div";
        className = "nof-propertylist";
        $parentEl: JQuery;
        editMode: bool;

        constructor (public model: Spiro.DomainObjectRepresentation, options? ) {
            super(options);

            this.el = undefined;
            this._ensureElement();
            _.bindAll(this, 'render');
            this.model.bind('change', this.render);
            this.model.bind('reset', this.render);

            this.delegateEvents();
        }

        render(): DomainObjectPropertiesView {

            _.each(toKeyValueArray(this.model.members()), function (keyValue: { key: string; value: Member; }) {

                var view = PropertyView.createView(keyValue.value);

                if (view) {
                    view.memberId = keyValue.key;
                    view.editMode = this.editMode;
                    this.$el.append(view.render().el);
                }
            }, this);

            if (this.$parentEl) {
                this.$parentEl.append(this.$el);
            }
            return this;
        }
    }

    class DomainObjectView extends Backbone.View {
        tagName = "div";
        className = "nof-objectview";
        $parentEl: JQuery;
        editMode: bool;

        constructor (public model: Spiro.DomainObjectRepresentation, options? ) {
            super(options);

            this.el = undefined;
            this._ensureElement();
            _.bindAll(this, 'renderEdit', 'renderView', 'render', 'requestFailed', 'requestSucceeded');
            this.model.bind('change', this.render);
            this.model.bind('reset', this.renderView);
            this.model.bind('requestFailed', this.requestFailed);
            this.model.bind('requestSucceeded', this.requestSucceeded);

            this.delegateEvents();
        }

        events = {
            "click :button": "select"
        }

        edit(): bool {
            this.editMode = true;
            this.render();
            return false;
        }

        view(): bool {
            this.editMode = false;
            this.render();
            return false;
        }

        save(): bool {
            var map = this.model.getUpdateMap();

            for (var attr in map.attributes) {
                var property = $(".nof-property#" + attr);
                map.setProperty(attr, getFieldInput(property)); 
            }

            map.save();
            return false;
        }

        requestSucceeded(): void {
            this.editMode = false;
            this.render();
        }

        renderError(resp: any): string {
            return Tags.Tag("h2", "", resp.message).toString() + Tags.Tag("p", "", resp.stackTrace);
        }

        renderValidationError(msg: string): string {
            return Tags.Tag("span", "field-validation-error", msg).toString();
        }

        requestFailed(resp) {
            this.editMode = true;

            if (resp.statusCode === 500) {
                displayOnMain().html(this.renderError(resp.model));
            } else {
                $(".field-validation-error").remove();

                for (var parm in resp.model.attributes) {
                    var key = parm;
                    var value = resp.model.attributes[key].value;
                    var msg = resp.model.attributes[key].invalidReason;

                    if (msg) {
                        var nofvalue = $("#" + key).find(".nof-value, .nof-object");
                        nofvalue.append($(this.renderValidationError(msg)));
                    }
                }
            }
        }

        select(e: JQueryEventObject) {

            // todo chnage to map 

            var currentTarget = <HTMLButtonElement> e.currentTarget;

            if (currentTarget.className === "nof-edit") {
                this.edit();
            } else if (currentTarget.className === "nof-cancel") {
                this.view();
            } else if (currentTarget.className === "nof-save") {
                this.save();
            } else {
                var objectAction = this.model.actionMembers()[currentTarget.form.id].getDetails();
                var dialogView = new DialogView(objectAction);
                dialogView.$parentEl = displayOnMain();
                dialogView.mode = { showAsLink: false, renderDialog: function (dv: DialogView) { return dv.renderDialog() }, $parentEl: function (o) { return null; } };

                objectAction.fetch();
            }

            return false;
        }

        renderView(): void {
            this.editMode = false;
        }

        renderEdit(): void {
            this.editMode = true;
        }

        renderObject(): string {

            var imgTag = Tags.Tag("img");
            imgTag.mergeAttribute("src", "/Content/Default.png").mergeAttribute("alt", "");

            var divTag = Tags.Tag("div", "nof-object", imgTag);

            divTag.innerHtml += this.model.title();

            return divTag.toString();
        }

        renderMenu(): string {

            var menuTag = Tags.Tag("div", "nof-menu", Tags.Tag("div", "nof-menuname", "Actions"));
            menuTag.mergeAttribute("id", shortName(this.model.domainType()) + "-Actions");

            var itemsTag = Tags.Tag("div", "nof-menuitems");

            for (var a in this.model.actionMembers()) {
                var action = this.model.actionMembers()[a];

                var formTag = Tags.Tag("form", "nof-action", Tags.Tag("div", "", Tags.Tag("button", "", action.extensions().friendlyName)));
                formTag.mergeAttribute("id", a);

                itemsTag.innerHtml += formTag;
            }

            menuTag.innerHtml += itemsTag;
            return menuTag.toString();
        }


        render(): DomainObjectView {

            if (Object.keys(this.model.attributes).length === 0) {
                displayOnMain(); // clears main 
                return this;
            }

            if (this.editMode) {
                this.$el.addClass("nof-objectedit");
                this.$el.removeClass("nof-objectview");
            }
            else {
                this.$el.removeClass("nof-objectedit");
                this.$el.addClass("nof-objectview");
            }

            this.$el.html(this.renderObject());

            if (!this.editMode) {
                this.$el.append(this.renderMenu());
            }

            var pView = new DomainObjectPropertiesView(this.model);
            pView.editMode = this.editMode;
            this.$el.append(pView.render().el);

            if (this.editMode) {
                this.$el.append("<form class='nof-action'><div><button class='nof-save'>Save</button></div></form>");
                this.$el.append("<form class='nof-action'><div><button class='nof-cancel'>Cancel</button></div></form>");
            } else {
                this.$el.append("<form class='nof-action'><div><button class='nof-edit'>Edit</button></div></form>");
            }

            this.$el.find(".nof-property > .nof-object[data-hasFinder]").each(function () : JQuery{
                var serviceLinks = home.getDomainServices();
                var findMenuView = new FindMenuView(serviceLinks);
                findMenuView.$parentEl = $(this);
                serviceLinks.fetch();
                return null;
            });

            if (this.$parentEl.find(".nof-objectview, .nof-objectedit").length == 0) {
                this.$parentEl.html(this.$el);
            }

            this.$parentEl.find(".date").datepicker();

            return this;
        }
    }

    // display the result of a action invocation by delegating to teh correct view for the result type 
    class ResultView extends Backbone.View {
        tagName = "div";
        className = "nof-objectview";
        $parentEl: JQuery;
        showLink: bool;

        constructor (public model: Spiro.ActionResultRepresentation, options? ) {
            super(options);

            this.el = undefined;
            this._ensureElement();
            _.bindAll(this, 'render', 'error');
            this.model.bind('change', this.render);
            this.model.bind('reset', this.render);
            this.model.bind('error', this.error);
            this.model.bind('requestFailed', this.requestFailed);

            this.delegateEvents();
        }

        render() {
            var result = this.model.result();
            var resultType = this.model.resultType();

            // to do replace this with lookup table ? 
            if (result == null || resultType === "void") {
                displayOnMain(); // clears main 
            } else if (resultType === "object") {

                if (this.showLink) {
                    var domainObjectLinkView = new DomainObjectLinkView(result.object());
                    domainObjectLinkView.$parentEl = this.$parentEl;
                    domainObjectLinkView.render();
                } else {
                    var domainObjectView = new DomainObjectView(result.object());
                    domainObjectView.$parentEl = displayOnMain();
                    domainObjectView.render();
                }
            } else if (resultType === "scalar") {
                alert("unsupported display of scalar");
            } else if (resultType === "list") {

                if (this.showLink) {
                    var listLinkListView = new ListLinkView(result.list().value());
                    listLinkListView.$parentEl = this.$parentEl;
                    listLinkListView.render();
                } else {
                    var domainObjectListView = new DomainObjectListView(result.list().value());
                    domainObjectListView.render();
                }
            }


            return this;
        }

        renderError(resp: any): string {
            return Tags.Tag("h2", "", resp.message).toString() + Tags.Tag("p", "", resp.stackTrace);
        }

        requestFailed(resp: any) {
            if (resp.statusCode === 500) {
                displayOnMain().html(this.renderError(resp.model));
            } else {
                $(".field-validation-error").remove();

                for (var parm in resp.model.attributes) {
                    var key = parm;
                    var value = resp.model.attributes[key].value;
                    var msg = resp.model.attributes[key].invalidReason;

                    if (msg) {
                        var nofvalue = $("#" + key).find(".nof-value, .nof-object");
                        nofvalue.append($("<span class='field-validation-error'>" + msg + "</span>"));
                    }
                }
            }
        }

        error() {

        }
    }

    // a view of an individual action item in a find menu 
    // todo - should this handle its own 'select' ? 
    // todo this is a separate view becauase we want it to trigger the visibility of its parent 
    // if it is rendered - because it's in a find menu it's only rendered if it returns the correct type 
    // and has no or only scalar parameters 
    // todo should other menus follow the pattern of having a item view (for consistency?) or is it unnecessary ? 
    class FindSubMenuItemView extends Backbone.View {
        tagName = "div";
        $parentEl: JQuery;
        dataType;

        constructor (public model: Spiro.ActionRepresentation, options? ) {
            super(options);

            this.el = undefined;
            this._ensureElement();
            _.bindAll(this, 'render');
            this.model.bind('change', this.render);
            this.model.bind('reset', this.render);

            this.delegateEvents();
        }

        renderSubMenuItem(): string {
            if (isFindAction(this.model, this.dataType)) {
                var formTag = Tags.Tag("form", "nof-action", Tags.Tag("div", "", Tags.Tag("button", "", friendlyActionName(this.model))));
                formTag.mergeAttribute("id", this.model.actionId());
                return formTag.toString();
            }
            return "";
        }

        render(): FindSubMenuItemView {

            // render only if conditions are satisfied 
            var mi = this.renderSubMenuItem();

            if (mi) {
                // if we are rendered make parent visible 
                this.$parentEl.closest(".nof-submenu").show();
                this.$parentEl.append(mi);
            }
            return this;
        }
    }

    // a sub menu within the find menu for each service 
    class FindSubMenuView extends Backbone.View {
        tagName = "div";
        className = "nof-submenu";
        $parentEl: JQuery;
        dataType;

        constructor (public model: Spiro.DomainObjectRepresentation, options? ) {
            super(options);

            this.el = undefined;
            this._ensureElement();
            _.bindAll(this, 'render');
            this.model.bind('change', this.render);
            this.model.bind('reset', this.render);

            this.delegateEvents();
        }

        events = {
            "click :button": "select"
        }

        // an object action in the menu has been clicked on - invoke starting with a dialog view 
        // todo 1. should this be in the FindSubmenuitemview ? 
        select(e: JQueryEventObject) {

            var currentTarget = <HTMLButtonElement> e.currentTarget;
            var objectAction = this.model.actionMembers()[currentTarget.form.id].getDetails();
            var dialogView = new DialogView(objectAction);
            dialogView.$parentEl = this.$parentEl.closest(".nof-object");
            dialogView.mode = { showAsLink: true, renderDialog: function (dv: DialogView) { return dv.renderFindDialog() }, $parentEl: function (o) { return o.$parentEl; } };

            objectAction.fetch();

            return false;
        }

        renderSubMenu(): string {
            return Tags.Tag("div", "nof-menuname", this.model.extensions().friendlyName).toString() + Tags.Tag("div", "nof-submenuitems");
        }

        // each submenu will always render itself - but will remain invisible unless there are any 
        // active menu items within it. 
        render(): FindSubMenuView {

            this.$el.html(this.renderSubMenu());
            this.$parentEl.append(this.$el);

            this.$el.hide(); // make invisible 

            var objectActions: ActionRepresentation[] = _.map(_.toArray(this.model.actionMembers()), function (am: ActionMember) {
                return am.getDetails();
            }, this);

            // for each action create a matching view 
            _.each(objectActions, function (i: ActionRepresentation) {
                var view = new FindSubMenuItemView(i);
                view.$parentEl = this.$el.find(".nof-submenuitems");
                view.dataType = this.dataType;
                i.fetch();
            }, this);

            return this;
        }
    }

    // find menu for reference fields 
    class FindMenuView extends Backbone.View {
        tagName = "div";
        className = "nof-menu";
        $parentEl: JQuery;

        constructor (public model: Spiro.DomainServicesRepresentation, options? ) {
            super(options);

            this.el = undefined;
            this._ensureElement();
            _.bindAll(this, 'render');
            this.model.bind('change', this.render);
            this.model.bind('reset', this.render);

            this.delegateEvents();
        }

        events = {
            "click :button": "clickRemove"
        }

        // remove button clears value from associated field ($parentEl)
        clickRemove(e: JQueryEventObject) {
            this.$parentEl.find(" > img").remove();
            this.$parentEl.find(" > a").remove();
            return false;
        }

        renderFindMenu(): string { 
            var menuNameTag = Tags.Tag("div", "nof-menuname", "Find"); 
            var menuItemsTag = Tags.Tag("div", "nof-menuitems",  Tags.Tag("button", "nof-remove", "Remove"));         
            return menuNameTag.toString() + menuItemsTag;
        }

        // append this menu to the associated field ($parentEl) - always have a 'remove' menu item - 
        render(): FindMenuView {

            this.$el.html(this.renderFindMenu());
            this.$parentEl.append(this.$el);

            // build the menu out of submenus for each service 
            // todo - this is all very expensive and needs to be cached at some point 
            _.each(this.model.value().models, function (link: Link) {
                var service =  <DomainObjectRepresentation> link.getTarget();
                var findSubmenuView = new FindSubMenuView(service);
                findSubmenuView.$parentEl = this.$el.find(".nof-menuitems");
                findSubmenuView.dataType = this.$parentEl.attr("data-type");
                service.fetch();
            }, this);

            return this;
        }
    }

    interface DialogMode {
        showAsLink: bool;
        renderDialog: (dv: DialogView) => string;
        $parentEl: (o) => JQuery;
    }

    // an intermediate view - while invoking an action may need to render a dialog to accept parameters or may pass 
    // straight through if parameterless action. 
    // may display as a dialog alone on teh page or inline in an object after a find action 
    class DialogView extends Backbone.View {
        tagName = "div";
        className = "nof-actiondialog";
        $parentEl: JQuery;
        mode: DialogMode;

        constructor (public model: Spiro.ActionRepresentation, options? ) {
            super(options);

            this.el = undefined;
            this._ensureElement();
            _.bindAll(this, 'render');
            this.model.bind('change', this.render);
            this.model.bind('reset', this.render);

            this.delegateEvents();
        }

        events = {
            "click :button.nof-ok": "ok",
            "click :button.nof-cancel": "cancel"
        }

        // click ok on dialog - populate action result - ie invoke - with parm values and fetch from server  
        ok(): bool {

            var invoke = this.model.getInvoke();

            $(".nof-parameter").each(function () : JQuery{
                invoke.setParameter(this.id, getFieldInput($(this)));
                return null;
            });

            var resultView = new ResultView(invoke);
            resultView.showLink = this.mode.showAsLink;
            resultView.$parentEl = this.mode.$parentEl(this);

            invoke.fetch();

            return false;
        }

        // todo - need to return to previous view here 
        cancel(): bool {
            return false;
        }

        renderDialog(): string {

            var formTag = Tags.Tag("form", "nof-dialog");
            var divListTag = Tags.Tag("div", "nof-parameterlist");

            for (var p in this.model.parameters()) {
                var value = this.model.parameters()[p];

                var divParmTag = Tags.Tag("div", "nof-parameter", Tags.Tag("label", "", value.extensions().friendlyName));
                divParmTag.mergeAttribute("id", p);

                var divValueTag = Tags.Tag("div");
                if (value.isScalar()) {
                    divValueTag.addCssClass("nof-value");
                    divValueTag.innerHtml += Tags.Tag("input");
                }
                else {
                    divValueTag.addCssClass("nof-object");
                    divValueTag.mergeAttribute("data-type", value.extensions().returnType);
                }
                divValueTag.innerHtml += mandatoryIndicatorIfApplies(value);

                divParmTag.innerHtml += divValueTag;
                divListTag.innerHtml += divParmTag;
            }
            formTag.innerHtml += divListTag;

            var buttonTag = Tags.Tag("button", "nof-ok");
            buttonTag.setInnerText("OK");
            formTag.innerHtml += buttonTag;

            var divLinkTag = Tags.Tag("div", "nof-object", Tags.Tag("a"));
            var divNameTag = Tags.Tag("div", "nof-actionname", this.model.extensions().friendlyName);
            var cancelFormTag = Tags.Tag("form", "nof-action", Tags.Tag("div", "", Tags.Tag("button", "nof-cancel", "Cancel")));

            return divLinkTag.toString() + divNameTag.toString() + formTag.toString() + cancelFormTag.toString();
        }

        renderFindDialog(): string {

            var divListTag = Tags.Tag("div", "nof-parameterlist");

            for (var p in this.model.parameters()) {
                var value = this.model.parameters()[p];

                var divParmTag = Tags.Tag("div", "nof-parameter", Tags.Tag("label", "", value.extensions().friendlyName));
                divParmTag.mergeAttribute("id", p);

                var divValueTag = Tags.Tag("div");
                if (value.isScalar()) {
                    divValueTag.addCssClass("nof-value");
                    divValueTag.innerHtml += Tags.Tag("input");
                }
                else {
                    divValueTag.addCssClass("nof-object");
                    divValueTag.mergeAttribute("data-type", value.extensions().returnType);
                }
                divValueTag.innerHtml += mandatoryIndicatorIfApplies(value);

                divParmTag.innerHtml += divValueTag;
                divListTag.innerHtml += divParmTag;
            }


            return Tags.Tag("div", "nof-actionname", this.model.extensions().friendlyName).toString() +
                   divListTag +
                   Tags.Tag("button", "nof-ok", "OK") +
                   Tags.Tag("form", "nof-action", Tags.Tag("div", "", Tags.Tag("button", "nof-cancel", "Cancel")));
        }


        // if action has parmeters then render a dialog otherwise invoke the action at once and allow Result view to render 
        render(): DialogView {

            if (this.model.extensions().hasParams) {
                this.$el.html(this.mode.renderDialog(this));

                // create the find menu for any reference parameters 
                this.$el.find(".nof-parameter > .nof-object").each(function () : JQuery{
                    var serviceLinks = home.getDomainServices();
                    var findMenuView = new FindMenuView(serviceLinks);
                    findMenuView.$parentEl = this;
                    serviceLinks.fetch();
                    return null;
                });

                this.$parentEl.append(this.$el);
            } else {
                // if no parameters go straight though to invoke  

                var invoke = this.model.getInvoke();
                var resultView = new ResultView(invoke);
                resultView.showLink = this.mode.showAsLink;
                resultView.$parentEl = this.mode.$parentEl(this);
                invoke.fetch();
            }

            return this;
        }
    }

    // menu of a individual service on the menu bar 
    class ServiceMenuView extends Backbone.View {
        tagName = "div";
        className = "nof-menu";
        $parentEl: JQuery;

        constructor (public model: Spiro.DomainObjectRepresentation, options? ) {
            super(options);

            this.el = undefined;
            this._ensureElement();
            _.bindAll(this, 'render');
            this.model.bind('change', this.render);
            this.model.bind('reset', this.render);

            this.delegateEvents();
        }

        events = {
            "click :button": "select"
        };

        // start invoking an object action by getting the action member from the model 
        // and then getting its details - the dialog view will then handle any parameters 
        select(e: JQueryEventObject) {
            var currentTarget = <HTMLButtonElement> e.currentTarget;

            var objectAction = this.model.actionMembers()[currentTarget.form.id].getDetails();
        
            var dialogView = new DialogView(objectAction);

            dialogView.$parentEl = displayOnMain();
            dialogView.mode = { showAsLink: false, renderDialog: function (dv: DialogView) { return dv.renderDialog() }, $parentEl: function (o) { return $(""); } };

            objectAction.fetch();
            return false;
        }

        renderServiceMenu(): string {

            var itemsTag = Tags.Tag("div", "nof-menuitems");

            for (var m in this.model.actionMembers()) {
                var value: ActionMember = this.model.actionMembers()[m];

                if (value.disabledReason()) {
                    var reasonTag = Tags.Tag("div", "nof-action", friendlyActionName(value));
                    reasonTag.mergeAttribute("title", value.disabledReason());
                    itemsTag.innerHtml += reasonTag.toString();
                }
                else {
                    var formTag = Tags.Tag("form", "nof-action", Tags.Tag("div", "", Tags.Tag("button", "", friendlyActionName(value))));
                    formTag.mergeAttribute("id", m);
                    itemsTag.innerHtml += formTag.toString();
                }
            }

            var titleTag = Tags.Tag("div", "nof-menuname", this.model.title());
            return titleTag.toString() + itemsTag.toString();
        }

        // render this service menu onto the menu bar ($parentEl) 
        render(): ServiceMenuView {
            this.$el.html(this.renderServiceMenu());
            this.$el.attr("id", this.model.serviceId());
            this.$parentEl.append(this.$el);

            return this;
        }
    }

    // services menu bar across top of page 
    class MenuBarView extends Backbone.View {
        tagName = "div";
        className = "nof-servicelist";

        constructor (public model: Spiro.DomainServicesRepresentation, options? ) {
            super(options);

            this.el = undefined;
            this._ensureElement();
            _.bindAll(this, 'render');
            this.model.bind('change', this.render);
            this.model.bind('reset', this.render);
        }

        render(): MenuBarView {
            // for each service link get the actual service so it renders itself as a menu on the menu bar
            _.each(this.model.value().models, function (link: Link) {
                var service = <DomainObjectRepresentation>link.getTarget();
                var serviceView = new ServiceMenuView(service);
                serviceView.$parentEl = this.$el;
                service.fetch();
            }, this);

            $("#header").append(this.$el);
            return this;
        }
    }

    // home page 
    class HomeView extends Backbone.View {

        constructor (public model: Spiro.HomePageRepresentation, options? ) {
            super(options);
            _.bindAll(this, 'render');
            this.model.bind('change', this.render);
            this.model.bind('reset', this.render);
        }

        // at the moment home page is just a menu bar and warning message 
        render(): HomeView {
            displayOnMain().html("This demo currently only works IE9+, and is mainly being tested in Firefox 16");

            var serviceLinks = this.model.getDomainServices();
            var menuBarView = new MenuBarView(serviceLinks);
            serviceLinks.fetch();
            return this;
        }
    }

    class AppRouter extends Backbone.Router {

        constructor (options) {
            super(options);
        }

        home() {
            home = new Spiro.HomePageRepresentation();
            var homeView = new HomeView(home);
            home.fetch();
        }
    }

    var app = new AppRouter({ "routes": { "": "home" } });

    Backbone.history.start();

    window.onerror = function (msg, url, linenumber) {
        alert('Error message: ' + msg + '\nURL: ' + url + '\nLine Number: ' + linenumber);
        return true;
    };

    var bindAjaxError = function () {
        $("div#main").ajaxError(function (e, xhr, settings) {
            //  errorDialog('Ajax Error', "Error in: " + settings.url + " \n" + "error:\n" + xhr.responseText);

            alert('ajax error');
        });
    };
}





































































































































































